using System;
using System.Transactions;
using System.Configuration;
using System.Globalization;

using System.Data;
using System.Data.SqlClient;

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using System.Collections;
using System.Collections.Generic;

using System.Security;
using System.Security.Principal;

namespace CodePlex.Diagnostics.Providers
{
    /// <summary>
    /// Specialized <see cref="T:CodePlex.Diagnostics.Providers.ExceptionProviderBase"/> that allows exceptions to be published to a SQL Server 2005 database.
    /// </summary>
    public sealed class SqlExceptionProvider : ExceptionProviderBase
    {
        #region public override void Publish(object context)

        /// <summary>
        /// Publishes the specified <see cref="T:CodePlex.Diagnostics.ExceptionContext"/> instance.
        /// </summary>
        /// <param name="context">The <see cref="T:CodePlex.Diagnostics.ExceptionContext"/> to be published.</param>
        public override void Publish(object context)
        {
            this.ExceptionContext = context as ExceptionContext;

            if (this.ExceptionContext != null)
            {
                this.Publish(this.ExceptionContext.Exception, this.ExceptionContext.IIdentity);
            }
        }

        #endregion

        #region public override void Publish(Exception ex, IIdentity identity)

        /// <summary>
        /// Publishes the specified <see cref="T:System.Exception"/> instance.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this exception.</param>
        public override void Publish(Exception ex, IIdentity identity)
        {
            Stack<Exception> exceptions = new Stack<Exception>();

            int? innerExceptionId = null;

            while (ex.InnerException != null)
            {
                exceptions.Push(ex);

                ex = ex.InnerException;
            }

            exceptions.Push(ex);

            do
            {
                this.Publish(exceptions.Pop(),
                             identity,
                             ref innerExceptionId);
            }
            while (exceptions.Count > 0);
        }

        #endregion

        #region private void Publish(Exception ex, IIdentity identity, ref int? innerExceptionId)

        /// <summary>
        /// Publishes the specified <see cref="T:System.Exception"/> instance.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this exception.</param>
        /// <param name="innerExceptionId">The inner exception id.</param>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        private void Publish(Exception ex, IIdentity identity, ref int? innerExceptionId)
        {
            int? machineId = null;
            int? assemblyId = null;
            int? targetSiteId = null;
            int? threadId = null;
            int? appDomainId = null;
            int? processId = null;

            try
            {
                using (TransactionScope scope = new TransactionScope())
                {
                    using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["CodePlex.Diagnostics"].ConnectionString))
                    {
                        // open the connection and enlist the connection in the transaction using the lightweight transaction manager.
                        connection.Open();

                        #region using (SqlCommand command = new SqlCommand("usp_MachineInsert", connection))

                        using (SqlCommand command = new SqlCommand("usp_MachineInsert", connection))
                        {
                            command.CommandType = CommandType.StoredProcedure;

                            command.Parameters.Add(new SqlParameter("@MachineId", SqlDbType.Int, 4));
                            command.Parameters["@MachineId"].Direction = ParameterDirection.Output;

                            command.Parameters.AddWithValue("@MachineName", ExceptionContext.MachineName);
                            command.Parameters.AddWithValue("@MachineIPAddress", ExceptionContext.IPAddress.ToString());
                            command.Parameters.AddWithValue("@OSVersion", ExceptionContext.OperatingSystem);
                            command.Parameters.AddWithValue("@ProcessorCount", ExceptionContext.ProcessorCount);

                            // retrieve details about the installed memory modules.
                            IList<MemoryModule> modules = MemoryModule.GetMemoryModules();

                            int speed = modules[0].Speed;

                            long capacity = 0;

                            foreach (MemoryModule module in modules)
                            {
                                // calculate the total capacity of installed memory modules.
                                capacity += module.Capacity;

                                // in cases where memory speed is not the same the slowest memory will be used.
                                if (speed > module.Speed)
                                {
                                    speed = module.Speed;
                                }
                            }

                            command.Parameters.AddWithValue("@InstalledMemory", capacity);
                            command.Parameters.AddWithValue("@InstalledMemorySpeed", speed);

                            command.ExecuteNonQuery();

                            machineId = Convert.ToInt32(command.Parameters["@MachineId"].Value, CultureInfo.CurrentCulture);
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_ProcessorInsert", connection))

                        foreach (Processor processor in Processor.GetProcessors())
                        {
                            using (SqlCommand command = new SqlCommand("usp_ProcessorInsert", connection))
                            {
                                command.CommandType = CommandType.StoredProcedure;

                                command.Parameters.Add(new SqlParameter("@ProcessorId", SqlDbType.Int, 4));
                                command.Parameters["@ProcessorId"].Direction = ParameterDirection.Output;

                                command.Parameters.AddWithValue("@MachineId", machineId);
                                command.Parameters.AddWithValue("@DeviceId", processor.DeviceId);
                                command.Parameters.AddWithValue("@Architecture", processor.Architecture);
                                command.Parameters.AddWithValue("@Name", processor.Name);
                                command.Parameters.AddWithValue("@Description", processor.Description);
                                command.Parameters.AddWithValue("@Manufacturer", processor.Manufacturer);

                                // Win32_Processor class NumberOfCores property does not exist on versions of the Windows operating system prior to Windows Vista and Windows 2008 (codename Longhorn),
                                // as such this column will be null for exceptions and log entries published on machines running earlier versions of the Windows operating system.
                                if (processor.NumberOfCores.HasValue)
                                {
                                    command.Parameters.AddWithValue("@Cores", processor.NumberOfCores);
                                }
                                else
                                {
                                    command.Parameters.AddWithValue("@Cores", DBNull.Value);
                                }

                                command.ExecuteNonQuery();
                            } 
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_GraphicsProcessorInsert", connection))

                        foreach (GraphicsProcessor processor in GraphicsProcessor.GetGraphicsProcessors())
                        {
                            using (SqlCommand command = new SqlCommand("usp_GraphicsProcessorInsert", connection))
                            {
                                command.CommandType = CommandType.StoredProcedure;

                                command.Parameters.Add(new SqlParameter("@GraphicsProcessorId", SqlDbType.Int, 4));
                                command.Parameters["@GraphicsProcessorId"].Direction = ParameterDirection.Output;

                                command.Parameters.AddWithValue("@MachineId", machineId);
                                command.Parameters.AddWithValue("@DeviceId", processor.DeviceId);
                                command.Parameters.AddWithValue("@Name", processor.Name);
                                command.Parameters.AddWithValue("@Description", processor.Description);
                                command.Parameters.AddWithValue("@InstalledMemory", processor.Memory);

                                if (processor.DriverVersion == null)
                                {
                                    command.Parameters.AddWithValue("@DriverVersion", DBNull.Value);
                                }
                                else
                                {
                                    command.Parameters.AddWithValue("@DriverVersion", processor.DriverVersion.ToString());
                                }

                                command.ExecuteNonQuery();
                            }
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_TargetSiteInsert", connection))

                        if (ex.TargetSite != null)
                        {
                            using (SqlCommand command = new SqlCommand("usp_TargetSiteInsert", connection))
                            {
                                command.CommandType = CommandType.StoredProcedure;

                                command.Parameters.Add(new SqlParameter("@TargetSiteId", SqlDbType.Int, 4));
                                command.Parameters["@TargetSiteId"].Direction = ParameterDirection.Output;

                                command.Parameters.AddWithValue("@IsAssembly", ex.TargetSite.IsAssembly);

                                command.Parameters.AddWithValue("@IsAbstract", ex.TargetSite.IsAbstract);
                                command.Parameters.AddWithValue("@IsConstructor", ex.TargetSite.IsConstructor);
                                command.Parameters.AddWithValue("@IsFamily", ex.TargetSite.IsFamily);
                                command.Parameters.AddWithValue("@IsFamilyAndAssembly", ex.TargetSite.IsFamilyAndAssembly);
                                command.Parameters.AddWithValue("@IsFamilyOrAssembly", ex.TargetSite.IsFamilyOrAssembly);
                                command.Parameters.AddWithValue("@IsFinal", ex.TargetSite.IsFinal);
                                command.Parameters.AddWithValue("@IsHideBySignature", ex.TargetSite.IsHideBySig);
                                command.Parameters.AddWithValue("@IsPrivate", ex.TargetSite.IsPrivate);
                                command.Parameters.AddWithValue("@IsPublic", ex.TargetSite.IsPublic);
                                command.Parameters.AddWithValue("@IsSpecialName", ex.TargetSite.IsSpecialName);
                                command.Parameters.AddWithValue("@IsStatic", ex.TargetSite.IsStatic);
                                command.Parameters.AddWithValue("@IsVirtual", ex.TargetSite.IsVirtual);

                                command.ExecuteNonQuery();

                                targetSiteId = Convert.ToInt32(command.Parameters["@TargetSiteId"].Value, CultureInfo.CurrentCulture);
                            }
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_AssemblyInsert", connection))

                        using (SqlCommand command = new SqlCommand("usp_AssemblyInsert", connection))
                        {
                            command.CommandType = CommandType.StoredProcedure;

                            command.Parameters.Add(new SqlParameter("@AssemblyId", SqlDbType.Int, 4));
                            command.Parameters["@AssemblyId"].Direction = ParameterDirection.Output;

                            command.Parameters.AddWithValue("@CodeBase", this.ExceptionContext.Assembly.CodeBase);

                            if (this.ExceptionContext.Assembly.EntryPoint == null)
                            {
                                command.Parameters.AddWithValue("@EntryPoint", DBNull.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@EntryPoint", this.ExceptionContext.Assembly.EntryPoint.Name);
                            }
                            
                            command.Parameters.AddWithValue("@FullName", this.ExceptionContext.Assembly.FullName);
                            command.Parameters.AddWithValue("@GlobalAssemblyCache", this.ExceptionContext.Assembly.GlobalAssemblyCache);
                            command.Parameters.AddWithValue("@HostContext", this.ExceptionContext.Assembly.HostContext);
                            command.Parameters.AddWithValue("@ImageRuntimeVersion", this.ExceptionContext.Assembly.ImageRuntimeVersion);
                            command.Parameters.AddWithValue("@Location", this.ExceptionContext.Assembly.Location);
                            command.Parameters.AddWithValue("@ManifestModule", this.ExceptionContext.Assembly.ManifestModule.FullyQualifiedName);
                            command.Parameters.AddWithValue("@DebugBuild", this.ExceptionContext.AssemblyDebugBuild);

                            command.ExecuteNonQuery();

                            assemblyId = Convert.ToInt32(command.Parameters["@AssemblyId"].Value, CultureInfo.CurrentCulture);
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_ThreadInsert", connection))

                        using (SqlCommand command = new SqlCommand("usp_ThreadInsert", connection))
                        {
                            command.CommandType = CommandType.StoredProcedure;

                            command.Parameters.Add(new SqlParameter("@ThreadId", SqlDbType.Int, 4));
                            command.Parameters["@ThreadId"].Direction = ParameterDirection.Output;

                            command.Parameters.AddWithValue("@IsAlive", this.ExceptionContext.Thread.IsAlive);
                            command.Parameters.AddWithValue("@IsBackground", this.ExceptionContext.Thread.IsBackground);
                            command.Parameters.AddWithValue("@IsThreadPoolThread", this.ExceptionContext.Thread.IsThreadPoolThread);
                            command.Parameters.AddWithValue("@ManagedThreadId", this.ExceptionContext.Thread.ManagedThreadId);

                            if (string.IsNullOrEmpty(this.ExceptionContext.Thread.Name))
                            {
                                command.Parameters.AddWithValue("@Name", DBNull.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@Name", this.ExceptionContext.Thread.Name);
                            }
                            
                            command.Parameters.AddWithValue("@Priority", this.ExceptionContext.Thread.Priority.ToString());
                            command.Parameters.AddWithValue("@ThreadState", this.ExceptionContext.Thread.ThreadState.ToString());
                            command.Parameters.AddWithValue("@CurrentCulture", this.ExceptionContext.Thread.CurrentCulture.ToString());
                            command.Parameters.AddWithValue("@CurrentUICulture", this.ExceptionContext.Thread.CurrentUICulture.ToString());

                            command.ExecuteNonQuery();

                            threadId = Convert.ToInt32(command.Parameters["@ThreadId"].Value, CultureInfo.CurrentCulture);
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_AppDomainInsert", connection))

                        using (SqlCommand command = new SqlCommand("usp_AppDomainInsert", connection))
                        {
                            command.CommandType = CommandType.StoredProcedure;

                            command.Parameters.Add(new SqlParameter("@AppDomainId", SqlDbType.Int, 4));
                            command.Parameters["@AppDomainId"].Direction = ParameterDirection.Output;

                            command.Parameters.AddWithValue("@BaseDirectory", this.ExceptionContext.AppDomain.BaseDirectory);

                            if (this.ExceptionContext.AppDomain.DomainManager != null)
                            {
                                command.Parameters.AddWithValue("@DomainManager", this.ExceptionContext.AppDomain.DomainManager.ToString());
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@DomainManager", DBNull.Value);
                            }

                            command.Parameters.AddWithValue("@FriendlyName", this.ExceptionContext.AppDomain.FriendlyName);
                            command.Parameters.AddWithValue("@Id", this.ExceptionContext.AppDomain.Id);

                            command.ExecuteNonQuery();

                            appDomainId = Convert.ToInt32(command.Parameters["@AppDomainId"].Value, CultureInfo.CurrentCulture);
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_ProcessInsert", connection))

                        using (SqlCommand command = new SqlCommand("usp_ProcessInsert", connection))
                        {
                            command.CommandType = CommandType.StoredProcedure;

                            command.Parameters.Add(new SqlParameter("@ProcessId", SqlDbType.Int, 4));
                            command.Parameters["@ProcessId"].Direction = ParameterDirection.Output;

                            command.Parameters.AddWithValue("@BasePriority", this.ExceptionContext.Process.BasePriority);

                            if (this.ExceptionContext.Process.HasExited)
                            {
                                command.Parameters.AddWithValue("@ExitCode", this.ExceptionContext.Process.ExitCode);
                                command.Parameters.AddWithValue("@ExitTime", this.ExceptionContext.Process.ExitTime);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@ExitCode", DBNull.Value);
                                command.Parameters.AddWithValue("@ExitTime", DBNull.Value);
                            }

                            command.Parameters.AddWithValue("@HasExited", this.ExceptionContext.Process.HasExited);

                            command.Parameters.AddWithValue("@Id", this.ExceptionContext.Process.Id);

                            command.Parameters.AddWithValue("@NonpagedSystemMemorySize64", this.ExceptionContext.Process.NonpagedSystemMemorySize64);
                            
                            command.Parameters.AddWithValue("@PagedMemorySize64", this.ExceptionContext.Process.PagedMemorySize64);

                            command.Parameters.AddWithValue("@PagedSystemMemorySize64", this.ExceptionContext.Process.PagedSystemMemorySize64);
                            
                            command.Parameters.AddWithValue("@PeakPagedMemorySize64", this.ExceptionContext.Process.PeakPagedMemorySize64);
                            
                            command.Parameters.AddWithValue("@PeakVirtualMemorySize64", this.ExceptionContext.Process.PeakVirtualMemorySize64);
                            
                            command.Parameters.AddWithValue("@PeakWorkingSet64", this.ExceptionContext.Process.PeakWorkingSet64);
                            
                            command.Parameters.AddWithValue("@PriorityBoostEnabled", this.ExceptionContext.Process.PriorityBoostEnabled);
                            
                            command.Parameters.AddWithValue("@PrivateMemorySize64", this.ExceptionContext.Process.PrivateMemorySize64);
                            
                            command.Parameters.AddWithValue("@PrivilegedProcessorTime", this.ExceptionContext.Process.PrivilegedProcessorTime.ToString());
                            
                            command.Parameters.AddWithValue("@ProcessName", this.ExceptionContext.Process.ProcessName);
                            
                            command.Parameters.AddWithValue("@ProcessorAffinity", this.ExceptionContext.Process.ProcessorAffinity.ToInt32());

                            command.Parameters.AddWithValue("@Responding", this.ExceptionContext.Process.Responding);

                            command.Parameters.AddWithValue("@SessionId", this.ExceptionContext.Process.SessionId);

                            command.Parameters.AddWithValue("@StartTime", this.ExceptionContext.Process.StartTime);

                            command.Parameters.AddWithValue("@Threads", this.ExceptionContext.Process.Threads.Count);

                            command.Parameters.AddWithValue("@TotalProcessorTime", this.ExceptionContext.Process.TotalProcessorTime.ToString());

                            command.Parameters.AddWithValue("@UserProcessorTime", this.ExceptionContext.Process.UserProcessorTime.ToString());

                            command.Parameters.AddWithValue("@VirtualMemorySize64", this.ExceptionContext.Process.VirtualMemorySize64);

                            command.Parameters.AddWithValue("@WorkingSet64", this.ExceptionContext.Process.WorkingSet64);

                            command.ExecuteNonQuery();

                            processId = Convert.ToInt32(command.Parameters["@ProcessId"].Value, CultureInfo.CurrentCulture);
                        }

                        #endregion

                        #region using (SqlCommand command = new SqlCommand("usp_ExceptionInsert", connection))

                        using (SqlCommand command = new SqlCommand("usp_ExceptionInsert", connection))
                        {
                            command.CommandType = CommandType.StoredProcedure;

                            command.Parameters.Add(new SqlParameter("@ExceptionId", SqlDbType.Int, 4));
                            command.Parameters["@ExceptionId"].Direction = ParameterDirection.Output;

                            command.Parameters.AddWithValue("@AssemblyId", assemblyId.Value);

                            command.Parameters.AddWithValue("@PublicationId", this.ExceptionContext.Id);

                            if (targetSiteId.HasValue)
                            {
                                command.Parameters.AddWithValue("@TargetSiteId", targetSiteId.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@TargetSiteId", DBNull.Value);
                            }

                            if (threadId.HasValue)
                            {
                                command.Parameters.AddWithValue("@ThreadId", threadId.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@ThreadId", DBNull.Value);
                            }

                            if (appDomainId.HasValue)
                            {
                                command.Parameters.AddWithValue("@AppDomainId", appDomainId.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@AppDomainId", DBNull.Value);
                            }

                            if (machineId.HasValue)
                            {
                                command.Parameters.AddWithValue("@MachineId", machineId.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@MachineId", DBNull.Value);
                            }
                            
                            if (string.IsNullOrEmpty(ex.HelpLink))
                            {
                                command.Parameters.AddWithValue("@HelpLink", DBNull.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@HelpLink", ex.HelpLink);
                            }

                            // it is necessary to store the type of the exception so that the serialized exception can be later deserialized.
                            command.Parameters.AddWithValue("@ExceptionType", ex.GetType().ToString());

                            int? result = GetHResult(ex);
                            command.Parameters.AddWithValue("@HResult", result.Value);

                            if (innerExceptionId.HasValue)
                            {
                                command.Parameters.AddWithValue("@InnerExceptionID", innerExceptionId.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@InnerExceptionID", DBNull.Value);
                            }

                            bool? transient = GetIsTransient(ex);
                            command.Parameters.AddWithValue("@IsTransient", transient.Value);

                            command.Parameters.AddWithValue("@Message", ex.Message);

                            if (string.IsNullOrEmpty(ex.Source))
                            {
                                command.Parameters.AddWithValue("@Source", DBNull.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@Source", ex.Source);
                            }

                            if (string.IsNullOrEmpty(ex.StackTrace))
                            {
                                command.Parameters.AddWithValue("@StackTrace", DBNull.Value);
                            }
                            else
                            {
                                // Work Item 6241: corrected issue in build 1.0.0.35 and earlier where the Source was being published in place of the StackTrace.
                                command.Parameters.AddWithValue("@StackTrace", ex.StackTrace);
                            }

                            command.Parameters.AddWithValue("@ProcessID", processId);
                            
                            string exceptionXml = ExceptionSerializer.Serialize(ex);

                            if (string.IsNullOrEmpty(exceptionXml))
                            {
                                command.Parameters.AddWithValue("@SerializedExceptionXml", DBNull.Value);
                            }
                            else
                            {
                                command.Parameters.AddWithValue("@SerializedExceptionXml", exceptionXml);
                            }

                            string identityXml = IIdentitySerializer.Serialize(identity);
                            command.Parameters.AddWithValue("@SerializedIIdentityXml", identityXml);

                            command.Parameters.AddWithValue("@IIdentityName", identity.Name);
                            command.Parameters.AddWithValue("@IIdentityIsAuthenticated", identity.IsAuthenticated);
                            command.Parameters.AddWithValue("@IIdentityAuthenticationType", identity.AuthenticationType);

                            command.ExecuteNonQuery();

                            innerExceptionId = Convert.ToInt32(command.Parameters["@ExceptionID"].Value, CultureInfo.CurrentCulture);

                            connection.Close();
                        }

                        #endregion

                        scope.Complete();
                    }
                }
            }
            catch (Exception _exeception)
            {
                Trace.WriteLine(_exeception.Message);
                Trace.WriteLine(_exeception.StackTrace);
            }
        }

        #endregion     
    }
}